package org.apache.sad.core.framework.exception;

import java.text.MessageFormat;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import javax.xml.soap.SOAPFactory;
import javax.xml.namespace.QName;
import javax.xml.rpc.soap.SOAPFaultException;
import javax.xml.soap.Detail;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.Name;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;


import org.apache.sad.core.framework.jaxrpc.ServiceContext;
import org.apache.sad.core.framework.jaxrpc.ServiceContextHandler;

public abstract class BaseException extends RuntimeException {
	
    private static final long serialVersionUID = 1L;
    protected final static Log log = LogFactory.getLog(BaseException.class);
    
    public static final QName CLIENT_FAULT_CODE = new QName(ServiceContext.SOAP11_NAMESPACE, "Client");
    public static final QName SERVER_FAULT_CODE = new QName(ServiceContext.SOAP11_NAMESPACE, "Server");
	
    private List<Message> messages = new ArrayList<Message>(); 
    
    private Map<String, String> aspects;
    
	protected BaseException(){}

	public BaseException(String code) {
		messages.add(new Message(code, null));
	}
	
	public BaseException(String code, Map<String, String> aspects) {
		messages.add(new Message(code, null));
        this.aspects = aspects;
	}	
	
    public BaseException(String code, Object[] args) {
        messages.add(new Message(code, args));
    } 
    
    public BaseException(String code, Object[] args, Map<String, String> aspects) {
        messages.add(new Message(code, args));
        this.aspects = aspects;
    }     

    public BaseException(String code, Object[] args, Throwable cause) {
    	super(cause);
        messages.add(new Message(code, args));
    }  

    public BaseException(String code, Object[] args, Map<String, String> aspects, Throwable cause) {
    	super(cause);
        messages.add(new Message(code, args));
        this.aspects = aspects;
    }      
    
    public BaseException(String code, Object[] args, String defaultMessage) {
        messages.add(new Message(code, args, defaultMessage));
    }
    
    
    public BaseException(String code, Object[] args, String defaultMessage, Map<String, String> aspects) {
        messages.add(new Message(code, args, defaultMessage));
        this.aspects = aspects;
    }
    
    public BaseException(String code, Object[] args, String defaultMessage, Throwable cause) {
        super(cause);
    	messages.add(new Message(code, args, defaultMessage));
    }
    
    public BaseException(String code, Object[] args, String defaultMessage, Map<String, String> aspects, Throwable cause) {
        super(cause);
    	messages.add(new Message(code, args, defaultMessage));
    	this.aspects = aspects;
    }
    
    public BaseException(String code, Throwable cause) {
    	super(cause);
        messages.add(new Message(code, null));
    }   
    
    public BaseException(String code, Map<String, String> aspects, Throwable cause) {
    	super(cause);
        messages.add(new Message(code, null));
        this.aspects = aspects;
    }   
    
    public BaseException(List<Message> value) {        
        if (value != null) {
            messages = value;
        }
    }    
    
    public BaseException(List<Message> value, Map<String, String> aspects) {        
        if (value != null) {
            messages = value;
        }
        this.aspects = aspects;
    }    
        
    public BaseException(List<Message> messages, Throwable cause) {        
    	super(cause);
    	if ( messages != null ) {
    		this.messages = messages;
    	}
    }     
    
    public BaseException(List<Message> messages, Map<String, String> aspects, Throwable cause) {        
    	super(cause);
    	if ( messages != null ) {
    		this.messages = messages;
    	}
    	this.aspects = aspects;
    }        
    
    public void addMessage(Message msg) {
        this.messages.add(msg);
    }
    
    public void addMessages(Message[] msgs) {
    	if (msgs != null)
    		this.messages.addAll(Arrays.asList(msgs));
    }
    
    public Message[] getMessages() {
        return (Message[]) messages.toArray(new Message[messages.size()]);
    }
    
    public Map<String,String> getMessagesAsCodeTextMap() {
    	Map<String,String> aMap = new HashMap<String,String>();
    	for (Message aMessage: messages) 
    		aMap.put(aMessage.getCode(), aMessage.getText());
    	return aMap;
    }
    public String toString() {
        ToStringBuilder builder = new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE);
        for (Message message: messages) {
            builder.append("message", message.toString());
        }
        if ( aspects != null ) {
        	builder.append("aspects", this.aspects);
        }
        return builder.toString();
    }
    
	public static BaseException fromSOAPFault(SOAPFaultException soapFault) throws SOAPException {
		QName faultCode = soapFault.getFaultCode();
		BaseException result = null;
		
		List<Message> faultMessages = detailToMessages(soapFault.getDetail());
		if ( faultCode.equals(CLIENT_FAULT_CODE) ) {
			result = new ApplicationException(faultMessages);
		} else {
			result = new SystemException(faultMessages);
		}
		return result;
	}	
	
	/**
	 * Converts an ExceptionType thrown from a MDA service to ApplicationException.
	 * 
	 * @param exceptionType The ExceptionType. 
	 * @return BaseException
	 */
	public static BaseException fromExceptionType(Exception exceptionType) {
		if ( !exceptionType.getClass().getSimpleName().equals("ExceptionType") ) {
			throw new IllegalArgumentException("Exception has to be the type of ExceptionType");
		}
		List<Message> messageList = new ArrayList<Message>();
		try {
			Method getMessagesMethod = exceptionType.getClass().getMethod("getMessages");
			Object messagesType = getMessagesMethod.invoke(exceptionType);
			if ( messagesType != null ) {
				Method getMessageMethod = messagesType.getClass().getMethod("getMessage");
				Object[] messages = (Object[])getMessageMethod.invoke(messagesType);
				if ( messages != null && messages.length > 0 ) {
					Method getCodeMethod = messages[0].getClass().getMethod("getCode");
					Method getTextMethod = messages[0].getClass().getMethod("getText");
					for (Object message : messages) {
						Message msg = new Message((String)getCodeMethod.invoke(message));
						msg.setText((String)getTextMethod.invoke(message));
						messageList.add(msg);
					}
				}
			}
			
			return new ApplicationException(messageList);
		} catch (Exception e) {
			throw new IllegalArgumentException("ExceptionType is not a valid.", e);
		}
		
	}
	public SOAPFaultException toSOAPFaultException() {
		return toSOAPFaultException(null);
	}
	
	public SOAPFaultException toSOAPFaultException(MessageSource messageSource)  {
		return toSOAPFaultException(messageSource, false);
	}

	
	public SOAPFaultException toSOAPFaultException(MessageSource messageSource, boolean alwaysIncludeTransactionInfo)  {
		StringBuilder faultString = new StringBuilder();
		faultString.append(ServiceContext.FAULT_STRING);
		appendAspectsIfApplicable(messageSource, faultString, alwaysIncludeTransactionInfo);
		
		QName faultCode = null;
		if ( this instanceof ApplicationException ) {
			faultCode = CLIENT_FAULT_CODE;
		} else if ( this instanceof SystemException ) {
			faultCode = SERVER_FAULT_CODE;
		}
		
		Detail detail = createDetail(messageSource, this.getMessages());
		
		return new SOAPFaultException(faultCode, faultString.toString(), null, detail);
	}

	protected void appendAspectsIfApplicable(MessageSource messageSource, StringBuilder faultString, boolean alwaysIncludeTransactionInfo) {
		ServiceContext context = ServiceContextHandler.getInstance().getServiceContext();
		if ( this.aspects != null || alwaysIncludeTransactionInfo ) {
			faultString.append(" [ASPECTS: (");
			faultString.append("server=").append(context.getServerNode()).append(",");
			faultString.append("service=").append(context.getService()).append(",");
			faultString.append("version=").append(context.getServiceVersion()).append(",");
			faultString.append("operation=").append(context.getServiceOperation()).append(",");
			faultString.append("user=").append(context.getWsSecurityUser()).append(",");
			faultString.append("sourceSystemId=").append(context.getSourceSystemId()).append(",");
			faultString.append("messageId=").append(context.getMessageId()).append(",");
			faultString.append("trackingId=").append(context.getTrackingId());
			if ( this.aspects != null && !this.aspects.isEmpty() ) {
				for ( Entry<String, String> entry: aspects.entrySet()) {
					faultString.append(",");
					faultString.append(entry.getKey()).append("=").append(entry.getValue());
				}
			}
			appendNestedErrorMessages(messageSource, faultString);
			faultString.append(")]");
		}
	}

	private void appendNestedErrorMessages(MessageSource messageSource, StringBuilder faultString) {
		Throwable cause = getCause();
		while ( cause != null ) {
			if ( cause instanceof BaseException ) {
				Message[] messages = ((BaseException)cause).getMessages();
				if ( messages != null ) {
					for ( Message message : messages) {
						faultString.append(",");
						String messageText = message.getText();
						if ( messageText == null && messageSource != null) {
							try {
								messageText = messageSource.getMessage(message.getCode(), message.getArguments(), Locale.getDefault());
							} catch (Exception e) {
							}
						}
						faultString.append(message.getCode()).append("=").append(messageText);
					}
				}
			}
			cause = cause.getCause();
		}
	}
	
	private static List<Message> detailToMessages(Detail detail) throws SOAPException {
		if ( detail == null || detail.getDetailEntries() == null || !detail.getDetailEntries().hasNext()) {
			return null;
		}

        List<Message> result = new ArrayList<Message>();
        List<SOAPElement> serviceFaults = getChildElements(detail, "serviceFault");
        if ( serviceFaults.size() > 0) {
	        for (SOAPElement messagesElement : getChildElements(serviceFaults.get(0), "messages")) {
	            result.addAll(parseMessages(messagesElement));
	        }
        }
		return result;
	}    
	
    private static List<Message> parseMessages(SOAPElement messages)
            throws SOAPException {
        List<Message> results = new ArrayList<Message>();
        for (SOAPElement messageElement : getChildElements(messages, "message")) {
        	String code = getChildValue(messageElement, "code");
        	String text = getChildValue(messageElement, "text");
        	Message msg = new Message(code);
        	msg.setText(text);
        	results.add(msg);
        }
        return results;
    }	
    private static String getChildValue(SOAPElement parent,
            String childName) throws SOAPException {
        List<SOAPElement> childElements = getChildElements(parent, childName);

        if (childElements.size() == 0) {
            return "";
        }

        return childElements.get(0).getValue();
    }    
    private static List<SOAPElement> getChildElements(SOAPElement element, String localName) {
    	List<SOAPElement> result = new ArrayList<SOAPElement>();
    	Iterator<?> iterator = element.getChildElements();
    	while ( iterator.hasNext() ) {
    		Object child = iterator.next();
    		if ( child instanceof SOAPElement ) {
	    		SOAPElement childElement = (SOAPElement)child;
	    		if ( childElement.getLocalName().equals(localName) ) {
	    			result.add(childElement);
	    		}
    		}
    	}
    	return result;
    }
    
    // Copied from Exception Aspect
	public static Detail createDetail(MessageSource messageSource, Message[] messages) {
		try {
			Detail detail = SOAPFactory.newInstance().createDetail();			
			SOAPElement root = detail.addDetailEntry(createName("serviceFault"));

			if ( messages != null ) {
				SOAPElement errors = root.addChildElement(createName("messages"));

				for (Message message : messages ) {
					resolveErrorText(messageSource, message);

					SOAPElement entry = errors.addChildElement(createName("message"));
					if (StringUtils.isNotEmpty(message.getCode())) {
						entry.addChildElement(createName("code")).addTextNode(message.getCode());

						if ( message.getText() != null ) {
							entry.addChildElement(createName("text")).addTextNode(message.getText());
						}
					}
				}
			}
			return detail;
		} catch (Exception e) {
			log.error("Exception occurred during creating SOAP Detail", e);
			return null;
		}
	}

	/**
	 * Resolves the text in the Message instance.
	 */
	private static void resolveErrorText(MessageSource messageSource, Message errorMessage) {
		if ( errorMessage.getText() == null ) {
			String text = null;
			if ( messageSource != null ) {
				try {
					text = messageSource.getMessage(errorMessage.getCode(), errorMessage.getArguments(), Locale.getDefault());
				} catch ( NoSuchMessageException e) {
				}
			}
			if ( text == null ) {
				if ( errorMessage.getDefaultMessage() != null ) {
					text = MessageFormat.format(errorMessage.getDefaultMessage(), errorMessage.getArguments());
				}
			}
			errorMessage.setText(text);
		}
	}

	private static Name createName(String name) throws SOAPException {
		ServiceContext context = ServiceContextHandler.getInstance().getServiceContext();
		String namespace = context.getNamespace();
		if (StringUtils.isEmpty(namespace)) {
			namespace = ServiceContext.DEFAULT_DETAIL_NAMESPACE;
		}
		return SOAPFactory.newInstance().createName(name, ServiceContext.DETAIL_PREFIX,namespace);
	}
	
}
